SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
5 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.4 KB · 31 lines typescript
Raw Blame History
1import { NextRequest, NextResponse } from "next/server";2import { and, eq, sql } from "drizzle-orm";3import { db } from "@/db/client";4import { listingMedia } from "@/db/schema";5import { assertSameOrigin, requireUser } from "@/lib/auth/session";6import { getListingFull, assertOwner } from "@/lib/listings/service";7import { processUpload, MediaError, MAX_BYTES } from "@/lib/listings/media";8import { apiError, handle } from "@/lib/api";9import { audit } from "@/lib/auth/audit";10export const dynamic = "force-dynamic";11export async function PUT(req: NextRequest, { params }: { params: Promise<{ token: string }> }) {12  try {13    await assertSameOrigin(); const u = await requireUser(); const { token } = await params; const d = db();14    const [m] = await d.select().from(listingMedia).where(and(eq(listingMedia.uploadToken, token), eq(listingMedia.status, "UPLOADING"), sql`${listingMedia.uploadedAt} > now() - interval '15 minutes'`)).limit(1);15    if (!m) return apiError(404, "not_found", "Jeton d'envoi invalide ou expiré");16    const l = await getListingFull(m.listingId); if (!l) return apiError(404, "not_found", "Annonce introuvable"); assertOwner(l, u.id, u.roles);17    const len = Number(req.headers.get("content-length") ?? 0); if (len > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)");18    const buf = Buffer.from(await req.arrayBuffer()); if (buf.length > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)");19    try {20      const r = await processUpload(buf, req.headers.get("content-type"));21      await d.update(listingMedia).set({ storageKey: r.key, mimeType: "image/webp", width: r.width, height: r.height, bytes: r.bytes, scanStatus: "CLEAN", status: "READY", uploadToken: null }).where(eq(listingMedia.id, m.id));22      await audit("media.upload", { actorId: u.id, objectType: "listing_media", objectId: m.id, meta: { bytes: buf.length, mime: r.mime } });23      return NextResponse.json({ ok: true, mediaId: m.id, url: `/api/media/${r.key}/800.webp`, width: r.width, height: r.height });24    } catch (e) {25      await d.update(listingMedia).set({ status: "DELETED", scanStatus: "REJECTED", uploadToken: null }).where(eq(listingMedia.id, m.id));26      if (e instanceof MediaError) { await audit("media.rejected", { actorId: u.id, objectId: m.id, meta: { code: e.code } }); return apiError(422, e.code, e.message); }27      throw e;28    }29  } catch (e) { return handle(e); }30}31